From 45081dcb472f6aa197d6ebdc129485f77fe3ee88 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sun, 23 Nov 2025 11:20:18 +0000
Subject: feat(mobile): Add AI summary field to mobile bookmark info (#2157)

* feat: Add AI summary field to mobile bookmark info page

Add a new AI summary section to the mobile bookmark info page that allows users to:
- Generate AI summaries for link bookmarks
- View existing summaries with expand/collapse functionality
- Regenerate summaries with the refresh button
- Delete summaries

The implementation is inspired by the web app's AI summary feature and includes:
- Purple-themed styling to match the AI branding
- Loading states for all actions
- Toast notifications for success/error feedback
- Support for dark mode
- Only displays for LINK type bookmarks

The summary is rendered using markdown and appears prominently in the bookmark info page,
positioned between the title editor and tags section.

* refactor: Simplify AI summary styling to match normal fields

Remove purple border and background from the AI summary display to make it
look more like a standard field. The summary now uses the default card
background from InfoSection, making it visually consistent with other
fields on the page.

The purple "Summarize with AI" button is retained for the generate action.

* make the expand button more clear

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 .../mobile/app/dashboard/bookmarks/[slug]/info.tsx | 154 ++++++++++++++++++++-
 1 file changed, 153 insertions(+), 1 deletion(-)

(limited to 'apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx')

diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx
index 1781ec74..15e2a082 100644
--- a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx
+++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx
@@ -1,11 +1,12 @@
 import React from "react";
-import { Alert, Pressable, View } from "react-native";
+import { ActivityIndicator, Alert, Pressable, View } from "react-native";
 import {
   KeyboardAwareScrollView,
   KeyboardGestureArea,
 } from "react-native-keyboard-controller";
 import { useSafeAreaInsets } from "react-native-safe-area-context";
 import { router, Stack, useLocalSearchParams } from "expo-router";
+import BookmarkTextMarkdown from "@/components/bookmarks/BookmarkTextMarkdown";
 import TagPill from "@/components/bookmarks/TagPill";
 import FullPageError from "@/components/FullPageError";
 import { Button } from "@/components/ui/Button";
@@ -17,10 +18,12 @@ import { Skeleton } from "@/components/ui/Skeleton";
 import { Text } from "@/components/ui/Text";
 import { useToast } from "@/components/ui/Toast";
 import { cn } from "@/lib/utils";
+import { ChevronUp, RefreshCw, Sparkles, Trash2 } from "lucide-react-native";
 
 import {
   useAutoRefreshingBookmarkQuery,
   useDeleteBookmark,
+  useSummarizeBookmark,
   useUpdateBookmark,
 } from "@karakeep/shared-react/hooks/bookmarks";
 import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
@@ -138,6 +141,154 @@ function NotesEditor({
   );
 }
 
+function AISummarySection({ bookmark }: { bookmark: ZBookmark }) {
+  const { toast } = useToast();
+  const [isExpanded, setIsExpanded] = React.useState(false);
+
+  const { mutate: summarize, isPending: isSummarizing } = useSummarizeBookmark({
+    onSuccess: () => {
+      toast({
+        message: "Summary generated successfully!",
+        showProgress: false,
+      });
+    },
+    onError: () => {
+      toast({
+        message: "Failed to generate summary",
+        showProgress: false,
+      });
+    },
+  });
+
+  const { mutate: resummarize, isPending: isResummarizing } =
+    useSummarizeBookmark({
+      onSuccess: () => {
+        toast({
+          message: "Summary regenerated successfully!",
+          showProgress: false,
+        });
+      },
+      onError: () => {
+        toast({
+          message: "Failed to regenerate summary",
+          showProgress: false,
+        });
+      },
+    });
+
+  const { mutate: updateBookmark, isPending: isDeletingSummary } =
+    useUpdateBookmark({
+      onSuccess: () => {
+        toast({
+          message: "Summary deleted!",
+          showProgress: false,
+        });
+      },
+      onError: () => {
+        toast({
+          message: "Failed to delete summary",
+          showProgress: false,
+        });
+      },
+    });
+
+  // Only show for LINK bookmarks
+  if (bookmark.content.type !== BookmarkTypes.LINK) {
+    return null;
+  }
+
+  // If there's a summary, show it
+  if (bookmark.summary) {
+    return (
+      <InfoSection>
+        <View className={isExpanded ? "" : "max-h-20 overflow-hidden"}>
+          <BookmarkTextMarkdown text={bookmark.summary} />
+        </View>
+        {!isExpanded && (
+          <Pressable
+            onPress={() => setIsExpanded(true)}
+            className="rounded-md bg-gray-100 py-2 dark:bg-gray-800"
+          >
+            <Text className="text-center text-sm font-medium text-gray-600 dark:text-gray-400">
+              Show more
+            </Text>
+          </Pressable>
+        )}
+        {isExpanded && (
+          <View className="mt-2 flex flex-row justify-end gap-2">
+            <Pressable
+              onPress={() => resummarize({ bookmarkId: bookmark.id })}
+              disabled={isResummarizing}
+              className="rounded-full bg-gray-200 p-2 dark:bg-gray-700"
+            >
+              {isResummarizing ? (
+                <ActivityIndicator size="small" />
+              ) : (
+                <RefreshCw
+                  size={16}
+                  className="text-gray-600 dark:text-gray-400"
+                />
+              )}
+            </Pressable>
+            <Pressable
+              onPress={() =>
+                updateBookmark({ bookmarkId: bookmark.id, summary: null })
+              }
+              disabled={isDeletingSummary}
+              className="rounded-full bg-gray-200 p-2 dark:bg-gray-700"
+            >
+              {isDeletingSummary ? (
+                <ActivityIndicator size="small" />
+              ) : (
+                <Trash2
+                  size={16}
+                  className="text-gray-600 dark:text-gray-400"
+                />
+              )}
+            </Pressable>
+            <Pressable
+              onPress={() => setIsExpanded(false)}
+              className="rounded-full bg-gray-200 p-2 dark:bg-gray-700"
+            >
+              <ChevronUp
+                size={16}
+                className="text-gray-600 dark:text-gray-400"
+              />
+            </Pressable>
+          </View>
+        )}
+      </InfoSection>
+    );
+  }
+
+  // If no summary, show button to generate one
+  return (
+    <InfoSection>
+      <Pressable
+        onPress={() => summarize({ bookmarkId: bookmark.id })}
+        disabled={isSummarizing}
+        className="rounded-lg bg-purple-500 p-3 dark:bg-purple-600"
+      >
+        <View className="flex flex-row items-center justify-center gap-2">
+          {isSummarizing ? (
+            <>
+              <ActivityIndicator size="small" color="#fff" />
+              <Text className="font-medium text-white">
+                Generating summary...
+              </Text>
+            </>
+          ) : (
+            <>
+              <Text className="font-medium text-white">Summarize with AI</Text>
+              <Sparkles size={16} color="#fff" />
+            </>
+          )}
+        </View>
+      </Pressable>
+    </InfoSection>
+  );
+}
+
 const ViewBookmarkPage = () => {
   const insets = useSafeAreaInsets();
   const { slug } = useLocalSearchParams();
@@ -266,6 +417,7 @@ const ViewBookmarkPage = () => {
             }
             isPending={isEditPending}
           />
+          <AISummarySection bookmark={bookmark} />
           <TagList bookmark={bookmark} />
           <ManageLists bookmark={bookmark} />
           <NotesEditor
-- 
cgit v1.3-1-g0d28

